// Copyright (c) 2024 CHANGLEI. All rights reserved.

import 'dart:math' as math;
import 'dart:math';

import 'package:charts_grasp/src/chart/axis/axis.dart';
import 'package:charts_grasp/src/chart/axis/category_axis.dart';
import 'package:charts_grasp/src/chart/axis/datetime_axis.dart';
import 'package:charts_grasp/src/chart/axis/datetime_category_axis.dart';
import 'package:charts_grasp/src/chart/base/chart_base.dart';
import 'package:charts_grasp/src/chart/chart_series/series_renderer_properties.dart';
import 'package:charts_grasp/src/chart/chart_series/xy_data_series.dart';
import 'package:charts_grasp/src/chart/common/cartesian_state_properties.dart';
import 'package:charts_grasp/src/chart/common/marker.dart';
import 'package:charts_grasp/src/chart/utils/enum.dart';
import 'package:charts_grasp/src/chart/utils/helper.dart';
import 'package:charts_grasp/src/common/utils/enum.dart';
import 'package:charts_grasp/src/common/utils/typedef.dart';
import 'package:flutter/material.dart';

export 'package:charts_grasp/src/core/core.dart' show DataMarkerType, TooltipAlignment;

class Trendline {
  Trendline({
    this.enableTooltip = true,
    this.intercept,
    this.name,
    this.dashArray,
    this.color = Colors.blue,
    this.type = TrendlineType.linear,
    this.backwardForecast = 0,
    this.forwardForecast = 0,
    this.opacity = 1,
    this.isVisible = true,
    this.width = 2,
    this.animationDuration = 1500,
    this.animationDelay = 0,
    this.valueField = 'high',
    this.isVisibleInLegend = true,
    this.legendIconType = LegendIconType.horizontalLine,
    this.markerSettings = const MarkerSettings(),
    this.polynomialOrder = 2,
    this.period = 2,
    this.onRenderDetailsUpdate,
  });

  final double animationDuration;

  final double? animationDelay;

  final double backwardForecast;

  final double forwardForecast;

  final double width;

  final double opacity;

  final List<double>? dashArray;

  final bool enableTooltip;

  final Color color;

  final String? name;

  final double? intercept;

  final bool isVisible;

  final LegendIconType legendIconType;

  final TrendlineType type;

  final String valueField;

  final MarkerSettings markerSettings;

  final bool isVisibleInLegend;

  final int polynomialOrder;

  final int period;

  final ChartTrendlineRenderCallback? onRenderDetailsUpdate;

  @override
  bool operator ==(Object other) {
    if (identical(this, other)) {
      return true;
    }
    if (other.runtimeType != runtimeType) {
      return false;
    }

    return other is Trendline &&
        other.enableTooltip == enableTooltip &&
        other.intercept == intercept &&
        other.name == name &&
        other.dashArray == dashArray &&
        other.color == color &&
        other.type == type &&
        other.backwardForecast == backwardForecast &&
        other.forwardForecast == forwardForecast &&
        other.opacity == opacity &&
        other.isVisible == isVisible &&
        other.width == width &&
        other.animationDuration == animationDuration &&
        other.valueField == valueField &&
        other.isVisibleInLegend == isVisibleInLegend &&
        other.legendIconType == legendIconType &&
        other.markerSettings == markerSettings &&
        other.polynomialOrder == polynomialOrder &&
        other.period == period;
  }

  @override
  int get hashCode {
    final List<Object?> values = <Object?>[
      enableTooltip,
      intercept,
      name,
      dashArray,
      color,
      type,
      backwardForecast,
      forwardForecast,
      opacity,
      isVisible,
      width,
      animationDuration,
      valueField,
      isVisibleInLegend,
      legendIconType,
      markerSettings,
      polynomialOrder,
      period,
    ];
    return Object.hashAll(values);
  }
}

class TrendlineRenderer {
  TrendlineRenderer(this.trendline) {
    opacity = trendline.opacity;
    dashArray = trendline.dashArray;
    fillColor = trendline.color;
    visible = trendline.isVisible;
    name = trendline.name;
  }

  final Trendline trendline;

  List<CartesianChartPoint<dynamic>>? pointsData;

  _SlopeIntercept slopeIntercept = _SlopeIntercept();

  _SlopeIntercept slopeInterceptData = _SlopeIntercept();

  List<dynamic>? polynomialSlopes;

  List<double>? polynomialSlopesData;

  late List<Path> markerShapes;

  late List<Offset> points;

  late double opacity;

  List<double>? dashArray;

  late Color fillColor;

  late bool visible;

  String? name;

  late bool isNeedRender;

  late AnimationController animationController;

  bool isTrendlineRenderEvent = false;

  late SeriesRendererDetails _seriesRendererDetails;

  final DateTime excelDate = DateTime(1900);

  CartesianChartPoint<dynamic> getDataPoint(dynamic x, num y, CartesianChartPoint<dynamic> sourcePoint,
      SeriesRendererDetails seriesRendererDetails, int index,) {
    final CartesianChartPoint<dynamic> trendPoint = CartesianChartPoint<dynamic>(x, y);
    trendPoint.x = (seriesRendererDetails.xAxisDetails is DateTimeAxisRenderer)
        ? DateTime.fromMillisecondsSinceEpoch(x.floor())
        : x;
    trendPoint.y = y;
    trendPoint.xValue = x;
    trendPoint.pointColorMapper = _seriesRendererDetails.series.color;
    trendPoint.index = index;
    trendPoint.yValue = y;
    trendPoint.isVisible = true;
    seriesRendererDetails.minimumX = math.min(seriesRendererDetails.minimumX!, trendPoint.xValue);
    seriesRendererDetails.minimumY = math.min(seriesRendererDetails.minimumY!, trendPoint.yValue);
    seriesRendererDetails.maximumX = math.max(seriesRendererDetails.maximumX!, trendPoint.xValue);
    seriesRendererDetails.maximumY = math.max(seriesRendererDetails.maximumY!, trendPoint.yValue);
    return trendPoint;
  }

  List<CartesianChartPoint<dynamic>> getLinearPoints(List<CartesianChartPoint<dynamic>> points, dynamic xValues,
      List<num> yValues, SeriesRendererDetails seriesRendererDetails, _SlopeIntercept slopeInterceptLinear,) {
    num x1Linear;
    num x2Linear;
    final List<CartesianChartPoint<dynamic>> pts = <CartesianChartPoint<dynamic>>[];
    xValues.sort();
    if (seriesRendererDetails.xAxisDetails is DateTimeAxisRenderer) {
      x1Linear = _increaseDateTimeForecast(
          seriesRendererDetails.xAxisDetails as DateTimeAxisRenderer, xValues[0], -trendline.backwardForecast,);
      x2Linear = _increaseDateTimeForecast(seriesRendererDetails.xAxisDetails as DateTimeAxisRenderer,
          xValues[xValues.length - 1], trendline.forwardForecast,);
    } else {
      x1Linear = xValues[0] - trendline.backwardForecast;
      x2Linear = xValues[xValues.length - 1] + trendline.forwardForecast;
    }
    final num y1Linear = slopeInterceptLinear.slope! * x1Linear + slopeInterceptLinear.intercept!;
    final num y2Linear = slopeInterceptLinear.slope! * x2Linear + slopeInterceptLinear.intercept!;
    pts.add(getDataPoint(x1Linear, y1Linear, points[0], seriesRendererDetails, pts.length));
    pts.add(getDataPoint(x2Linear, y2Linear, points[points.length - 1], seriesRendererDetails, pts.length));
    return pts;
  }

  void _setLinearRange(List<CartesianChartPoint<dynamic>> points, SeriesRendererDetails seriesRendererDetails) {
    final List<dynamic> xValues = <dynamic>[];
    final List<int> slopeInterceptXValues = <int>[];
    final List<num> yValues = <num>[];
    int index = 0;
    const int startXValue = 1;
    while (index < points.length) {
      final CartesianChartPoint<dynamic> point = points[index];
      xValues.add(point.xValue ?? point.x);
      slopeInterceptXValues.add((seriesRendererDetails.xAxisDetails?.axisRenderer is DateTimeAxisRenderer)
          ? point.x.difference(excelDate).inDays
          : startXValue + index,);
      yValues.add(point.yValue ?? point.y);

      index++;
    }
    slopeIntercept = _findSlopeIntercept(xValues, yValues, points);
    if (!slopeIntercept.slope!.isNaN && !slopeIntercept.intercept!.isNaN) {
      pointsData = getLinearPoints(points, xValues, yValues, seriesRendererDetails, slopeIntercept);
    }
    slopeInterceptData = _findSlopeIntercept(slopeInterceptXValues, yValues, points);
  }

  List<CartesianChartPoint<dynamic>> getExponentialPoints(List<CartesianChartPoint<dynamic>> points, dynamic xValues,
      List<num> yValues, SeriesRendererDetails seriesRendererDetails, _SlopeIntercept slopeInterceptExpo,) {
    num x1;
    num x2;
    num x3;
    final int midPoint = (points.length / 2).round();
    final List<CartesianChartPoint<dynamic>> ptsExpo = <CartesianChartPoint<dynamic>>[];
    if (seriesRendererDetails.xAxisDetails is DateTimeAxisRenderer) {
      x1 = _increaseDateTimeForecast(
          seriesRendererDetails.xAxisDetails as DateTimeAxisRenderer, xValues[0], -trendline.backwardForecast,);
      x2 = xValues[midPoint - 1];
      x3 = _increaseDateTimeForecast(seriesRendererDetails.xAxisDetails as DateTimeAxisRenderer,
          xValues[xValues.length - 1], trendline.forwardForecast,);
    } else {
      x1 = xValues[0] - trendline.backwardForecast;
      x2 = xValues[midPoint - 1];
      x3 = xValues[xValues.length - 1] + trendline.forwardForecast;
    }
    final num y1 = slopeInterceptExpo.intercept! * math.exp(slopeInterceptExpo.slope! * x1);

    final num y2 = slopeInterceptExpo.intercept! * math.exp(slopeInterceptExpo.slope! * x2);

    final num y3 = slopeInterceptExpo.intercept! * math.exp(slopeInterceptExpo.slope! * x3);
    ptsExpo.add(getDataPoint(x1, y1.isNaN ? 0 : y1, points[0], seriesRendererDetails, ptsExpo.length));
    ptsExpo.add(getDataPoint(x2, y2.isNaN ? 0 : y2, points[midPoint - 1], seriesRendererDetails, ptsExpo.length));
    ptsExpo.add(getDataPoint(x2, y2.isNaN ? 0 : y2, points[midPoint - 1], seriesRendererDetails, ptsExpo.length));
    ptsExpo.add(getDataPoint(x3, y3.isNaN ? 0 : y3, points[points.length - 1], seriesRendererDetails, ptsExpo.length));
    if (y1.isNaN || y2.isNaN || y3.isNaN) {
      for (int i = 0; i < ptsExpo.length; i++) {
        ptsExpo[i].x = 0;
        ptsExpo[i].y = 0;
      }
    }
    return ptsExpo;
  }

  void _setExponentialRange(List<CartesianChartPoint<dynamic>> points, SeriesRendererDetails seriesRendererDetails) {
    final List<dynamic> xValues = <dynamic>[];
    final List<int> slopeInterceptXValues = <int>[];
    final List<num> yValues = <num>[];
    int index = 0;
    const int startXValue = 1;
    while (index < points.length) {
      final CartesianChartPoint<dynamic> point = points[index];
      xValues.add(point.xValue ?? point.x);
      slopeInterceptXValues.add(seriesRendererDetails.xAxisDetails?.axisRenderer is DateTimeAxisRenderer
          ? seriesRendererDetails.dataPoints[index].x.difference(excelDate).inDays
          : startXValue + index,);
      yValues.add(math.log(point.yValue ?? point.y));

      index++;
    }
    xValues.sort();
    slopeIntercept = _findSlopeIntercept(xValues, yValues, points);
    if (!slopeIntercept.slope!.isNaN && !slopeIntercept.intercept!.isNaN) {
      pointsData = getExponentialPoints(points, xValues, yValues, seriesRendererDetails, slopeIntercept);
    }
    slopeInterceptData = _findSlopeIntercept(slopeInterceptXValues, yValues, points);
  }

  List<CartesianChartPoint<dynamic>> getPowerPoints(List<CartesianChartPoint<dynamic>> points, dynamic xValues,
      List<num> yValues, SeriesRendererDetails seriesRendererDetails, _SlopeIntercept slopeInterceptPow,) {
    num x1;
    num x2;
    num x3;
    final int midPoint = (points.length / 2).round();
    final List<CartesianChartPoint<dynamic>> ptsPow = <CartesianChartPoint<dynamic>>[];
    if (seriesRendererDetails.xAxisDetails is DateTimeAxisRenderer) {
      x1 = _increaseDateTimeForecast(
          seriesRendererDetails.xAxisDetails as DateTimeAxisRenderer, xValues[0], -trendline.backwardForecast,);
      x2 = xValues[midPoint - 1];
      x3 = _increaseDateTimeForecast(seriesRendererDetails.xAxisDetails as DateTimeAxisRenderer,
          xValues[xValues.length - 1], trendline.forwardForecast,);
    } else {
      x1 = xValues[0] - trendline.backwardForecast;
      x1 = x1 > -1 ? x1 : 0;
      x2 = xValues[midPoint - 1];
      x3 = xValues[xValues.length - 1] + trendline.forwardForecast;
    }
    final num y1 = x1 == 0 ? 0 : slopeInterceptPow.intercept! * math.pow(x1, slopeInterceptPow.slope!);
    final num y2 = slopeInterceptPow.intercept! * math.pow(x2, slopeInterceptPow.slope!);
    final num y3 = slopeInterceptPow.intercept! * math.pow(x3, slopeInterceptPow.slope!);
    ptsPow.add(getDataPoint(x1, y1.isNaN ? 0 : y1, points[0], seriesRendererDetails, ptsPow.length));
    ptsPow.add(getDataPoint(x2, y2.isNaN ? 0 : y2, points[midPoint - 1], seriesRendererDetails, ptsPow.length));
    ptsPow.add(getDataPoint(x3, y3.isNaN ? 0 : y3, points[points.length - 1], seriesRendererDetails, ptsPow.length));
    if (y1.isNaN || y2.isNaN || y3.isNaN) {
      for (int i = 0; i < ptsPow.length; i++) {
        ptsPow[i].x = 0;
        ptsPow[i].y = 0;
      }
    }
    return ptsPow;
  }

  void _setPowerRange(List<CartesianChartPoint<dynamic>> points, SeriesRendererDetails seriesRendererDetails) {
    final List<dynamic> xValues = <dynamic>[];
    final List<num> slopeInterceptXValues = <num>[];
    final List<num> yValues = <num>[];
    final List<dynamic> powerPoints = <dynamic>[];
    int index = 0;
    const int startXValue = 1;
    while (index < points.length) {
      final CartesianChartPoint<dynamic> point = points[index];
      powerPoints.add(point.xValue ?? point.x);
      final dynamic xVal = point.xValue != null && math.log(point.xValue).isFinite
          ? math.log(point.xValue)
          : (seriesRendererDetails.xAxisDetails?.axisRenderer is CategoryAxisRenderer ||
                  seriesRendererDetails.xAxisDetails?.axisRenderer is DateTimeCategoryAxisRenderer)
              ? point.xValue
              : point.x;
      xValues.add(xVal);
      slopeInterceptXValues.add(math.log(seriesRendererDetails.xAxisDetails?.axisRenderer is DateTimeAxisRenderer
          ? seriesRendererDetails.dataPoints[index].x.difference(excelDate).inDays
          : startXValue + index,),);
      yValues.add(math.log(point.yValue ?? point.y));
      index++;
    }
    powerPoints.sort();
    slopeIntercept = _findSlopeIntercept(xValues, yValues, points);
    if (!slopeIntercept.slope!.isNaN && !slopeIntercept.intercept!.isNaN) {
      pointsData = getPowerPoints(points, powerPoints, yValues, seriesRendererDetails, slopeIntercept);
    }
    slopeInterceptData = _findSlopeIntercept(slopeInterceptXValues, yValues, points);
  }

  List<CartesianChartPoint<dynamic>> getLogarithmicPoints(List<CartesianChartPoint<dynamic>> points, dynamic xValues,
      List<num> yValues, SeriesRendererDetails seriesRendererDetails, _SlopeIntercept slopeInterceptLog,) {
    num x1;
    num x2;
    num x3;
    final int midPoint = (points.length / 2).round();
    final List<CartesianChartPoint<dynamic>> ptsLog = <CartesianChartPoint<dynamic>>[];
    if (seriesRendererDetails.xAxisDetails is DateTimeAxisRenderer) {
      x1 = _increaseDateTimeForecast(
          seriesRendererDetails.xAxisDetails as DateTimeAxisRenderer, xValues[0], -trendline.backwardForecast,);
      x2 = xValues[midPoint - 1];
      x3 = _increaseDateTimeForecast(seriesRendererDetails.xAxisDetails as DateTimeAxisRenderer,
          xValues[xValues.length - 1], trendline.forwardForecast,);
    } else {
      x1 = xValues[0] - trendline.backwardForecast;
      x2 = xValues[midPoint - 1];
      x3 = xValues[xValues.length - 1] + trendline.forwardForecast;
    }
    final num y1 =
        slopeInterceptLog.intercept! + (slopeInterceptLog.slope! * (math.log(x1).isFinite ? math.log(x1) : x1));
    final num y2 =
        slopeInterceptLog.intercept! + (slopeInterceptLog.slope! * (math.log(x2).isFinite ? math.log(x2) : x2));
    final num y3 =
        slopeInterceptLog.intercept! + (slopeInterceptLog.slope! * (math.log(x3).isFinite ? math.log(x3) : x3));
    ptsLog.add(getDataPoint(x1, y1, points[0], seriesRendererDetails, ptsLog.length));
    ptsLog.add(getDataPoint(x2, y2, points[midPoint - 1], seriesRendererDetails, ptsLog.length));
    ptsLog.add(getDataPoint(x3, y3, points[points.length - 1], seriesRendererDetails, ptsLog.length));
    return ptsLog;
  }

  void _setLogarithmicRange(List<CartesianChartPoint<dynamic>> points, SeriesRendererDetails seriesRendererDetails) {
    final List<dynamic> xLogValue = <dynamic>[];
    final List<num> slopeInterceptXLogValue = <num>[];
    final List<num> yLogValue = <num>[];
    final List<dynamic> xPointsLgr = <dynamic>[];
    int index = 0;
    const int startXValue = 1;
    while (index < points.length) {
      final CartesianChartPoint<dynamic> point = points[index];
      xPointsLgr.add(point.xValue ?? point.x);
      final dynamic xVal = (point.xValue != null && math.log(point.xValue).isFinite)
          ? math.log(point.xValue)
          : (seriesRendererDetails.xAxisDetails?.axisRenderer is CategoryAxisRenderer ||
                  seriesRendererDetails.xAxisDetails?.axisRenderer is DateTimeCategoryAxisRenderer)
              ? point.xValue
              : point.x;
      xLogValue.add(xVal);
      slopeInterceptXLogValue.add(math.log(seriesRendererDetails.xAxisDetails?.axisRenderer is DateTimeAxisRenderer
          ? points[index].x.difference(excelDate).inDays
          : startXValue + index,),);
      yLogValue.add(point.yValue ?? point.y);
      index++;
    }
    xPointsLgr.sort();
    slopeIntercept = _findSlopeIntercept(xLogValue, yLogValue, points);
    if (!slopeIntercept.slope!.isNaN && !slopeIntercept.intercept!.isNaN) {
      pointsData = getLogarithmicPoints(points, xPointsLgr, yLogValue, seriesRendererDetails, slopeIntercept);
    }
    slopeInterceptData = _findSlopeIntercept(slopeInterceptXLogValue, yLogValue, points);
  }

  List<CartesianChartPoint<dynamic>> _getPolynomialPoints(List<CartesianChartPoint<dynamic>> points, dynamic xValues,
      List<num> yValues, SeriesRendererDetails seriesRendererDetails,) {
    const int startXValue = 1;
    List<CartesianChartPoint<dynamic>> pts = <CartesianChartPoint<dynamic>>[];
    polynomialSlopes = List<dynamic>.filled(trendline.polynomialOrder + 1, null);

    for (int i = 0; i < xValues.length; i++) {
      final dynamic xVal = xValues[i];
      final num yVal = yValues[i];
      for (int j = 0; j <= trendline.polynomialOrder; j++) {
        polynomialSlopes![j] ??= 0;
        polynomialSlopes![j] += pow(xVal.toDouble(), j) * yVal;
      }
    }

    final List<dynamic> matrix = _getMatrix(trendline, xValues);
    if (!_gaussJordanElimination(matrix, polynomialSlopes!)) {}
    pts = _getPoints(points, xValues, yValues, seriesRendererDetails);
    if (trendline.onRenderDetailsUpdate != null) {
      polynomialSlopesData = List<double>.filled(trendline.polynomialOrder + 1, 0);

      for (int i = 0; i < xValues.length; i++) {
        final num yVal = yValues[i];
        for (int j = 0; j <= trendline.polynomialOrder; j++) {
          polynomialSlopesData![j] += pow(
                  seriesRendererDetails.xAxisDetails?.axisRenderer is DateTimeAxisRenderer
                      ? points[i].x.difference(excelDate).inDays
                      : startXValue + i.toDouble(),
                  j,) *
              yVal;
        }
      }
      final List<num> xData = <num>[];
      for (int i = 0; i < xValues.length; i++) {
        xData.add(seriesRendererDetails.xAxisDetails?.axisRenderer is DateTimeAxisRenderer
            ? points[i].x.difference(excelDate).inDays
            : startXValue + i.toDouble(),);
      }
      final List<dynamic> matrix = _getMatrix(trendline, xData);
      if (!_gaussJordanElimination(matrix, polynomialSlopesData!)) {}
    }
    return pts;
  }

  List<dynamic> _getMatrix(Trendline trendline, dynamic xValues) {
    final List<dynamic> numArray = List<dynamic>.filled(2 * trendline.polynomialOrder + 1, null);
    final List<dynamic> matrix = List<dynamic>.filled(trendline.polynomialOrder + 1, null);
    for (int i = 0; i <= trendline.polynomialOrder; i++) {
      matrix[i] = List<dynamic>.filled(trendline.polynomialOrder + 1, null);
    }

    num num1 = 0;
    for (int nIndex = 0; nIndex < xValues.length; nIndex++) {
      final num d = xValues[nIndex];
      num num2 = 1.0;
      for (int j = 0; j < numArray.length; j++, num1++) {
        numArray[j] ??= 0;
        numArray[j] += num2;
        num2 *= d;
      }
    }

    for (int i = 0; i <= trendline.polynomialOrder; i++) {
      for (int j = 0; j <= trendline.polynomialOrder; j++) {
        matrix[i][j] = numArray[i + j];
      }
    }
    return matrix;
  }

  void _setPolynomialRange(List<CartesianChartPoint<dynamic>> points, SeriesRendererDetails seriesRendererDetails) {
    final List<dynamic> xPolyValues = <dynamic>[];
    final List<num> yPolyValues = <num>[];
    int index = 0;
    while (index < points.length) {
      final CartesianChartPoint<dynamic> point = points[index];
      xPolyValues.add(point.xValue ?? point.x);
      yPolyValues.add(point.yValue ?? point.y);
      index++;
    }
    xPolyValues.sort();
    pointsData = _getPolynomialPoints(points, xPolyValues, yPolyValues, seriesRendererDetails);
  }

  List<CartesianChartPoint<dynamic>> _getPoints(List<CartesianChartPoint<dynamic>> points, dynamic xValues,
      List<num> yValues, SeriesRendererDetails seriesRendererDetails,) {
    final List<dynamic> polynomialSlopesList = polynomialSlopes!;
    final List<CartesianChartPoint<dynamic>> pts = <CartesianChartPoint<dynamic>>[];

    num x1 = 1;
    dynamic xVal;
    num yVal;
    final num backwardForecast = seriesRendererDetails.xAxisDetails is DateTimeAxisDetails
        ? _getForecastDate(seriesRendererDetails.xAxisDetails!, false)
        : trendline.backwardForecast;
    final num forwardForecast = seriesRendererDetails.xAxisDetails is DateTimeAxisDetails
        ? _getForecastDate(seriesRendererDetails.xAxisDetails!, true)
        : trendline.forwardForecast;

    for (int index = 1; index <= polynomialSlopesList.length; index++) {
      if (index == 1) {
        xVal = xValues[0] - backwardForecast.toDouble();
        yVal = _getPolynomialYValue(polynomialSlopesList, xVal);
        pts.add(getDataPoint(xVal, yVal, points[0], seriesRendererDetails, pts.length));
      } else if (index == polynomialSlopesList.length) {
        xVal = xValues[points.length - 1] + forwardForecast.toDouble();
        yVal = _getPolynomialYValue(polynomialSlopesList, xVal);
        pts.add(getDataPoint(xVal, yVal, points[points.length - 1], seriesRendererDetails, pts.length));
      } else {
        x1 += (points.length + trendline.forwardForecast) / polynomialSlopesList.length;
        xVal = xValues[x1.floor() - 1] * 1.0;
        yVal = _getPolynomialYValue(polynomialSlopesList, xVal);
        pts.add(getDataPoint(xVal, yVal, points[x1.floor() - 1], seriesRendererDetails, pts.length));
      }
    }
    return pts;
  }

  double _getPolynomialYValue(List<dynamic> slopes, dynamic x) {
    double sum = 0;
    for (int i = 0; i < slopes.length; i++) {
      sum += slopes[i] * pow(x, i);
    }
    return sum;
  }

  List<CartesianChartPoint<dynamic>> getMovingAveragePoints(List<CartesianChartPoint<dynamic>> points,
      List<dynamic> xValues, List<num?> yValues, SeriesRendererDetails seriesRendererDetails,) {
    final List<CartesianChartPoint<dynamic>> pts = <CartesianChartPoint<dynamic>>[];
    int periods = trendline.period >= points.length ? points.length - 1 : trendline.period;
    periods = max(2, periods);
    double? y;
    dynamic x;
    int count;
    int nullCount;
    for (int index = 0; index < points.length - 1; index++) {
      y = 0.0;
      count = nullCount = 0;
      for (int j = index; count < periods; j++) {
        count++;
        if (j >= yValues.length || yValues[j] == null) {
          nullCount++;
        }
        y = y! + (j >= yValues.length ? 0 : yValues[j]!);
      }
      y = ((periods - nullCount) <= 0) ? null : (y! / (periods - nullCount));
      if (y != null && !y.isNaN && index + periods < xValues.length + 1) {
        x = xValues[periods - 1 + index];
        pts.add(getDataPoint(x, y, points[periods - 1 + index], seriesRendererDetails, pts.length));
      }
    }
    return pts;
  }

  void _setMovingAverageRange(List<CartesianChartPoint<dynamic>> points, SeriesRendererDetails seriesRendererDetails) {
    final List<dynamic> xValues = <dynamic>[];
    final List<dynamic> xAvgValues = <dynamic>[];
    final List<num> yValues = <num>[];

    for (int index = 0; index < points.length; index++) {
      final dynamic point = points[index];
      xAvgValues.add(point.xValue ?? point.x);
      xValues.add(index + 1);
      yValues.add(point.yValue ?? point.y);
    }
    xAvgValues.sort();
    pointsData = getMovingAveragePoints(points, xAvgValues, yValues, seriesRendererDetails);
  }

  _SlopeIntercept _findSlopeIntercept(dynamic xValues, dynamic yValues, List<CartesianChartPoint<dynamic>> points) {
    double xAvg = 0;
    double yAvg = 0;
    double xyAvg = 0;
    double xxAvg = 0;
    int index = 0;
    double slope = 0;
    double intercept = 0;
    while (index < points.length) {
      if (yValues[index].isNaN == true) {
        yValues[index] = (yValues[index - 1] + yValues[index + 1]) / 2;
      }
      xAvg += xValues[index];
      yAvg += yValues[index];
      xyAvg += xValues[index].toDouble() * yValues[index].toDouble();
      xxAvg += xValues[index].toDouble() * xValues[index].toDouble();
      index++;
    }
    if (trendline.intercept != null &&
        trendline.intercept != 0 &&
        (trendline.type == TrendlineType.linear || trendline.type == TrendlineType.exponential)) {
      intercept = trendline.intercept!;
      switch (trendline.type) {
        case TrendlineType.linear:
          slope = (xyAvg - (intercept * xAvg)) / xxAvg;
          break;
        case TrendlineType.exponential:
          slope = (xyAvg - (math.log(intercept.abs()) * xAvg)) / xxAvg;
          break;
        default:
          break;
      }
    } else {
      slope = ((points.length * xyAvg) - (xAvg * yAvg)) / ((points.length * xxAvg) - (xAvg * xAvg));

      intercept = (trendline.type == TrendlineType.exponential || trendline.type == TrendlineType.power)
          ? math.exp((yAvg - (slope * xAvg)) / points.length)
          : (yAvg - (slope * xAvg)) / points.length;
    }
    slopeIntercept.slope = slope;
    slopeIntercept.intercept = intercept;
    return slopeIntercept;
  }

  void _initDataSource(CgCartesianChart chart, SeriesRendererDetails seriesRendererDetails) {
    if (pointsData!.isNotEmpty) {
      switch (trendline.type) {
        case TrendlineType.linear:
          _setLinearRange(pointsData!, seriesRendererDetails);
          break;
        case TrendlineType.exponential:
          _setExponentialRange(pointsData!, seriesRendererDetails);
          break;
        case TrendlineType.power:
          _setPowerRange(pointsData!, seriesRendererDetails);
          break;
        case TrendlineType.logarithmic:
          _setLogarithmicRange(pointsData!, seriesRendererDetails);
          break;
        case TrendlineType.polynomial:
          _setPolynomialRange(pointsData!, seriesRendererDetails);
          break;
        case TrendlineType.movingAverage:
          _setMovingAverageRange(pointsData!, seriesRendererDetails);
          break;
      }
    }
  }

  void calculateTrendlinePoints(SeriesRendererDetails seriesRendererDetails, CartesianStateProperties stateProperties) {
    final Rect rect = calculatePlotOffset(
        stateProperties.chartAxis.axisClipRect,
        Offset(
            seriesRendererDetails.xAxisDetails!.axis.plotOffset, seriesRendererDetails.yAxisDetails!.axis.plotOffset,),);
    points = <Offset>[];
    if (seriesRendererDetails.series.trendlines != null && pointsData != null) {
      for (int i = 0; i < pointsData!.length; i++) {
        if (pointsData![i].x != null && pointsData![i].y != null) {
          final ChartLocation currentChartPoint = pointsData![i].markerPoint = calculatePoint(
              (seriesRendererDetails.xAxisDetails is DateTimeAxisRenderer) ? pointsData![i].xValue : pointsData![i].x,
              pointsData![i].y,
              seriesRendererDetails.xAxisDetails!,
              seriesRendererDetails.yAxisDetails!,
              stateProperties.requireInvertedAxis,
              seriesRendererDetails.series,
              rect,);
          points.add(Offset(currentChartPoint.x, currentChartPoint.y));
          pointsData![i].region = Rect.fromLTRB(points[i].dx, points[i].dy, points[i].dx, points[i].dy);
        }
      }
      _calculateMarkerShapesPoint(seriesRendererDetails);
    }
  }

  void _calculateMarkerShapesPoint(SeriesRendererDetails seriesRendererDetails) {
    markerShapes = <Path>[];
    for (int i = 0; i < pointsData!.length; i++) {
      final CartesianChartPoint<dynamic> point = pointsData![i];
      final DataMarkerType markerType = trendline.markerSettings.shape;
      final Size size = Size(trendline.markerSettings.width, trendline.markerSettings.height);
      markerShapes.add(getMarkerShapesPath(
          markerType, Offset(point.markerPoint!.x, point.markerPoint!.y), size, seriesRendererDetails,),);
    }
  }

  void setDataSource(SeriesRendererDetails? seriesRendererDetails, CgCartesianChart chart) {
    if (seriesRendererDetails?.series != null) {
      _seriesRendererDetails = seriesRendererDetails!;
      pointsData = seriesRendererDetails.dataPoints;
      _initDataSource(chart, _seriesRendererDetails);
    }
  }

  List<Offset> getControlPoints(List<Offset> dataPoints, int index) {
    List<num?> yCoef = <num?>[];
    final List<Offset> controlPoints = <Offset>[];
    final List<num> xValues = <num>[];
    final List<num> yValues = <num>[];
    for (int i = 0; i < dataPoints.length; i++) {
      xValues.add(dataPoints[i].dx);
      yValues.add(dataPoints[i].dy);
    }
    yCoef = naturalSpline(xValues, yValues, yCoef, xValues.length, SplineType.natural);
    return calculateControlPoints(
        xValues, yValues, yCoef[index]!.toDouble(), yCoef[index + 1]!.toDouble(), index, controlPoints,);
  }

  int _increaseDateTimeForecast(DateTimeAxisRenderer axisRenderer, int value, num interval) {
    final DateTimeAxis axis = AxisHelper.getAxisRendererDetails(axisRenderer).axis as DateTimeAxis;
    DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(value);
    switch (axis.intervalType) {
      case DateTimeIntervalType.years:
        dateTime = DateTime(dateTime.year + interval.floor(), dateTime.month, dateTime.day);
        break;
      case DateTimeIntervalType.months:
        dateTime = DateTime(dateTime.year, dateTime.month + interval.floor(), dateTime.day);
        break;
      case DateTimeIntervalType.days:
        dateTime = DateTime(dateTime.year, dateTime.month, dateTime.day + interval.floor());
        break;
      case DateTimeIntervalType.hours:
        dateTime = DateTime(dateTime.year, dateTime.month, dateTime.day, dateTime.hour + interval.floor());
        break;
      case DateTimeIntervalType.minutes:
        dateTime =
            DateTime(dateTime.year, dateTime.month, dateTime.day, dateTime.hour, dateTime.minute + interval.floor());
        break;
      case DateTimeIntervalType.seconds:
        dateTime = DateTime(dateTime.year, dateTime.month, dateTime.day, dateTime.hour, dateTime.minute,
            dateTime.second + interval.floor(),);
        break;
      case DateTimeIntervalType.milliseconds:
        dateTime = DateTime(dateTime.year, dateTime.month, dateTime.day, dateTime.hour, dateTime.minute,
            dateTime.second, dateTime.millisecond + interval.floor(),);
        break;
      case DateTimeIntervalType.auto:
        break;
    }
    return dateTime.millisecondsSinceEpoch;
  }

  bool _gaussJordanElimination(List<dynamic> matrix, List<dynamic> polynomialSlopesList) {
    final int length = matrix.length;
    final List<dynamic> numArray1 = List<dynamic>.filled(length, null);
    final List<dynamic> numArray2 = List<dynamic>.filled(length, null);
    final List<dynamic> numArray3 = List<dynamic>.filled(length, null);

    for (int index = 0; index < length; index++) {
      numArray3[index] = 0;
    }
    int index1 = 0;
    while (index1 < length) {
      num num1 = 0;
      int index2 = 0;
      int index3 = 0;
      int index4 = 0;
      while (index4 < length) {
        if (numArray3[index4] != 1) {
          int index5 = 0;
          while (index5 < length) {
            if (numArray3[index5] == 0 && matrix[index4][index5].abs() >= num1 == true) {
              num1 = matrix[index4][index5].abs();
              index2 = index4;
              index3 = index5;
            }
            ++index5;
          }
        }
        ++index4;
      }
      ++numArray3[index3];
      if (index2 != index3) {
        int index4_1 = 0;
        while (index4_1 < length) {
          final num num2 = matrix[index2][index4_1];
          matrix[index2][index4_1] = matrix[index3][index4_1];
          matrix[index3][index4_1] = num2;
          ++index4_1;
        }
        final num num3 = polynomialSlopesList[index2];
        polynomialSlopesList[index2] = polynomialSlopesList[index3];
        polynomialSlopesList[index3] = num3;
      }
      numArray2[index1] = index2;
      numArray1[index1] = index3;
      if (matrix[index3][index3] == 0.0) {
        return false;
      }
      final num num4 = 1.0 / matrix[index3][index3];
      matrix[index3][index3] = 1.0;
      int iindex4 = 0;
      while (iindex4 < length) {
        matrix[index3][iindex4] *= num4;
        ++iindex4;
      }
      polynomialSlopesList[index3] *= num4;
      int iandex4 = 0;
      while (iandex4 < length) {
        if (iandex4 != index3) {
          final num num2 = matrix[iandex4][index3];
          matrix[iandex4][index3] = 0.0;
          int index5 = 0;
          while (index5 < length) {
            matrix[iandex4][index5] -= matrix[index3][index5] * num2;
            ++index5;
          }
          polynomialSlopesList[iandex4] -= polynomialSlopesList[index3] * num2;
        }
        ++iandex4;
      }
      ++index1;
    }
    for (int iindex1 = length - 1; iindex1 >= 0; iindex1--) {
      if (numArray2[iindex1] != numArray1[iindex1]) {
        for (int iindex2 = 0; iindex2 < length; iindex2++) {
          final num number = matrix[iindex2][numArray2[iindex1]];
          matrix[iindex2][numArray2[iindex1]] = matrix[iindex2][numArray1[iindex1]];
          matrix[iindex2][numArray1[iindex1]] = number;
        }
      }
    }
    return true;
  }

  List<Offset> getPolynomialCurve(List<CartesianChartPoint<dynamic>> points,
      SeriesRendererDetails seriesRendererDetails, CartesianStateProperties stateProperties,) {
    final List<Offset> polyPoints = <Offset>[];
    final dynamic start = seriesRendererDetails.xAxisDetails is DateTimeAxisRenderer ? points[0].xValue : points[0].x;
    final dynamic end = seriesRendererDetails.xAxisDetails is DateTimeAxisRenderer
        ? points[points.length - 1].xValue
        : points[points.length - 1].xValue;
    for (dynamic x = start; polyPoints.length <= 100; x += (end - start) / 100) {
      final double y = _getPolynomialYValue(polynomialSlopes!, x);
      final ChartLocation position = calculatePoint(
          x,
          y,
          seriesRendererDetails.xAxisDetails!,
          seriesRendererDetails.yAxisDetails!,
          stateProperties.requireInvertedAxis,
          seriesRendererDetails.series,
          stateProperties.chartAxis.axisClipRect,);
      polyPoints.add(Offset(position.x, position.y));
    }
    return polyPoints;
  }

  int _getForecastDate(ChartAxisRendererDetails axisRendererDetails, bool isForward) {
    Duration duration = Duration.zero;
    final DateTimeAxis axis = axisRendererDetails.axis as DateTimeAxis;
    switch (axis.intervalType) {
      case DateTimeIntervalType.auto:
        duration = Duration.zero;
        break;
      case DateTimeIntervalType.years:
        duration =
            Duration(days: (365.25 * (isForward ? trendline.forwardForecast : trendline.backwardForecast)).round());
        break;
      case DateTimeIntervalType.months:
        duration = Duration(days: 31 * (isForward ? trendline.forwardForecast : trendline.backwardForecast).round());
        break;
      case DateTimeIntervalType.days:
        duration = Duration(days: (isForward ? trendline.forwardForecast : trendline.backwardForecast).round());
        break;
      case DateTimeIntervalType.hours:
        duration = Duration(hours: (isForward ? trendline.forwardForecast : trendline.backwardForecast).round());
        break;
      case DateTimeIntervalType.minutes:
        duration = Duration(minutes: (isForward ? trendline.forwardForecast : trendline.backwardForecast).round());
        break;
      case DateTimeIntervalType.seconds:
        duration = Duration(seconds: (isForward ? trendline.forwardForecast : trendline.backwardForecast).round());
        break;
      case DateTimeIntervalType.milliseconds:
        duration = Duration(milliseconds: (isForward ? trendline.forwardForecast : trendline.backwardForecast).round());
        break;
    }
    return duration.inMilliseconds;
  }
}

class _SlopeIntercept {
  num? slope;
  num? intercept;
}
